#ifndef _geojsonvt_ioImp_H_
#define _geojsonvt_ioImp_H_

#if WIN32
#else
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpragmas"
#pragma GCC diagnostic ignored "-Wsign-conversion"
#pragma GCC diagnostic ignored "-Wsign-compare"
#pragma GCC diagnostic ignored "-Wconversion"
#pragma GCC diagnostic ignored "-Wold-style-cast"
#pragma GCC diagnostic ignored "-Wpadded"
#pragma GCC diagnostic ignored "-Wfloat-equal"
#endif
#include <mapbox/geojsonvt_io.hpp>
#include <mapbox/geojsonvt/tile.hpp>
#include <mapbox/geojsonvt/types.hpp>
#include <mapbox/geometry.hpp>
#include <mapbox/geometry_io.hpp>
#include <mapbox/variant_io.hpp>
#include <rapidjson/document.h>
#include <rapidjson/error/en.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#if WIN32
#else
#pragma GCC diagnostic pop
#endif
#include <fstream>
#include <map>
#include <sstream>

namespace mapbox {
namespace geojsonvt {
    using rapidjson_allocator = rapidjson::CrtAllocator;
    using rapidjson_document = rapidjson::GenericDocument<rapidjson::UTF8<>, rapidjson_allocator>;
    using rapidjson_value = rapidjson::GenericValue<rapidjson::UTF8<>, rapidjson_allocator>;

    // Convert inputs of known types. Instantiations are provided for geometry, feature, and
    // feature_collection.
    template <typename T>
    T convert(const rapidjson_value&);

    // Convert any GeoJSON type.
    geojson_vt convert(const rapidjson_value&);

    // Convert back to rapidjson value. Instantiations are provided for geometry, feature, and
    // feature_collection.
    template <typename T>
    rapidjson_value convert(const T&, rapidjson_allocator&);

    // Convert any GeoJSON type.
    rapidjson_value convert(const geojson_vt&, rapidjson_allocator&);

    using error = std::runtime_error;
    using prop_map = std::unordered_map<std::string, value>;

    void validatePolygon(const rapidjson_value& json) {
        // this check is required incase case of multipolygon validation
        if (!json.IsArray()) {
            throw error("Coordinates must be nested more deeply.");
        }
        for (auto& element : json.GetArray()) {
            if (!element.IsArray()) {
                throw error("Coordinates must be an array of arrays, each describing a polygon.");
            }
            if (element.Size() < 4) {
                throw error("Polygon must be described by 4 or more coordinate points. Improper "
                    "nesting can also lead to this error. Double check that the coordinates "
                    "are properly nested and there are 4 or more coordinates.");
            }
        }
    }

    void validateLineString(const rapidjson_value& json) {
        if (json.GetArray().Size() < 2) {
            throw error("A line string must have two or more coordinate points.");
        }
    }

    template <typename T>
    T convert(const rapidjson_value& json);

    template <>
    point_vt convert<point_vt>(const rapidjson_value& json) {
        if (!json.IsArray()) {
            throw error("coordinates must be an array.");
        }
        if (json.Size() < 2)
            throw error("coordinates array must have at least 2 numbers");

        return point_vt{ (int16_t)json[0].GetInt(), (int16_t)json[1].GetInt() };
    }

    template <typename Cont>
    Cont convert(const rapidjson_value& json) {
        Cont points;
        if (!json.IsArray()) {
            throw error("coordinates must be an array of points describing linestring or an array of "
                "arrays describing polygons and line strings.");
        }
        auto size = json.Size();
        points.reserve(size);

        for (auto& element : json.GetArray()) {
            points.push_back(convert<typename Cont::value_type>(element));
        }
        return points;
    }

    template <>
    geometry_vt convert<geometry_vt>(const rapidjson_value& json) {
        if (json.IsNull())
            return empty_vt{};

        if (!json.IsObject())
            throw error("Geometry must be an object");

        const auto& json_end = json.MemberEnd();

        const auto& type_itr = json.FindMember("type");
        if (type_itr == json_end)
            throw error("Geometry must have a type property");

        const auto& type = type_itr->value;

        if (type == "GeometryCollection") {
            const auto& geometries_itr = json.FindMember("geometries");
            if (geometries_itr == json_end)
                throw error("GeometryCollection must have a geometries property");

            const auto& json_geometries = geometries_itr->value;

            if (!json_geometries.IsArray())
                throw error("GeometryCollection geometries property must be an array");

            return geometry_vt{ convert<geometry_collection_vt>(json_geometries) };
        }

        const auto& coords_itr = json.FindMember("coordinates");

        if (coords_itr == json_end)
            throw error(std::string(type.GetString()) + " geometry must have a coordinates property");

        const auto& json_coords = coords_itr->value;
        if (!json_coords.IsArray())
            throw error("coordinates property must be an array");

        if (type == "Point")
            return geometry_vt{ convert<point_vt>(json_coords) };
        if (type == "MultiPoint")
            return geometry_vt{ convert<multi_point_vt>(json_coords) };
        if (type == "LineString") {
            validateLineString(json_coords);
            return geometry_vt{ convert<line_string_vt>(json_coords) };
        }
        if (type == "MultiLineString") {
            for (auto& element : json_coords.GetArray()) {
                validateLineString(element);
            }
            return geometry_vt{ convert<multi_line_string_vt>(json_coords) };
        }
        if (type == "Polygon") {
            validatePolygon(json_coords);
            return geometry_vt{ convert<polygon_vt>(json_coords) };
        }
        if (type == "MultiPolygon") {
            for (auto& element : json_coords.GetArray()) {
                validatePolygon(element);
            }
            return geometry_vt{ convert<multi_polygon_vt>(json_coords) };
        }
        throw error(std::string(type.GetString()) + " not yet implemented");
    }

    template <>
    value convert<value>(const rapidjson_value& json);

    template <>
    prop_map convert(const rapidjson_value& json) {
        if (!json.IsObject())
            throw error("properties must be an object");

        prop_map result;
        for (auto& member : json.GetObj()) {
            result.emplace(std::string(member.name.GetString(), member.name.GetStringLength()),
                convert<value>(member.value));
        }
        return result;
    }

    template <>
    value convert<value>(const rapidjson_value& json) {
        switch (json.GetType()) {
        case rapidjson::kNullType:
            return null_value_t{};
        case rapidjson::kFalseType:
            return false;
        case rapidjson::kTrueType:
            return true;
        case rapidjson::kObjectType:
            return convert<prop_map>(json);
        case rapidjson::kArrayType:
            return convert<std::vector<value>>(json);
        case rapidjson::kStringType:
            return std::string(json.GetString(), json.GetStringLength());
        default:
            assert(json.GetType() == rapidjson::kNumberType);
            if (json.IsUint64())
                return std::uint64_t(json.GetUint64());
            if (json.IsInt64())
                return std::int64_t(json.GetInt64());
            return json.GetDouble();
        }
    }

    template <>
    identifier convert<identifier>(const rapidjson_value& json) {
        switch (json.GetType()) {
        case rapidjson::kStringType:
            return std::string(json.GetString(), json.GetStringLength());
        case rapidjson::kNumberType:
            if (json.IsUint64())
                return std::uint64_t(json.GetUint64());
            if (json.IsInt64())
                return std::int64_t(json.GetInt64());
            return json.GetDouble();
        default:
            throw error("Feature id must be a string or number");
        }
    }

    template <>
    feature_vt convert<feature_vt>(const rapidjson_value& json) {
        if (!json.IsObject())
            throw error("Feature must be an object");

        auto const& json_end = json.MemberEnd();
        auto const& type_itr = json.FindMember("type");

        if (type_itr == json_end)
            throw error("Feature must have a type property");
        if (type_itr->value != "Feature")
            throw error("Feature type must be Feature");

        auto const& geom_itr = json.FindMember("geometry");

        if (geom_itr == json_end)
            throw error("Feature must have a geometry property");

        feature_vt result{ convert<geometry_vt>(geom_itr->value) };

        auto const& id_itr = json.FindMember("id");
        if (id_itr != json_end) {
            result.id = convert<identifier>(id_itr->value);
        }

        auto const& prop_itr = json.FindMember("properties");
        if (prop_itr != json_end) {
            const auto& json_props = prop_itr->value;
            if (!json_props.IsNull()) {
                result.properties = convert<prop_map>(json_props);
            }
        }

        return result;
    }

    template <>
    geojson_vt convert<geojson_vt>(const rapidjson_value& json) {
        if (!json.IsObject())
            throw error("GeoJSON must be an object");

        const auto& type_itr = json.FindMember("type");
        const auto& json_end = json.MemberEnd();

        if (type_itr == json_end)
            throw error("GeoJSON must have a type property");

        const auto& type = type_itr->value;

        if (type == "FeatureCollection") {
            const auto& features_itr = json.FindMember("features");
            if (features_itr == json_end)
                throw error("FeatureCollection must have features property");

            const auto& json_features = features_itr->value;

            if (!json_features.IsArray())
                throw error("FeatureCollection features property must be an array");

            feature_collection_vt collection;

            const auto& size = json_features.Size();
            collection.reserve(size);

            for (auto& feature_obj : json_features.GetArray()) {
                collection.push_back(convert<feature_vt>(feature_obj));
            }

            return geojson_vt{ collection };
        }

        if (type == "Feature")
            return geojson_vt{ convert<feature_vt>(json) };

        return geojson_vt{ convert<geometry_vt>(json) };
    }

    geojson_vt convert(const rapidjson_value& json) {
        return convert<geojson_vt>(json);
    }

    template <>
    rapidjson_value convert<geometry_vt>(const geometry_vt&, rapidjson_allocator&);

    template <>
    rapidjson_value convert<feature_vt>(const feature_vt&, rapidjson_allocator&);

    template <>
    rapidjson_value convert<feature_collection_vt>(const feature_collection_vt&, rapidjson_allocator&);

    struct to_type {
    public:
        const char* operator()(const empty_vt&) {
            abort();
        }

        const char* operator()(const point_vt&) {
            return "Point";
        }

        const char* operator()(const line_string_vt&) {
            return "LineString";
        }

        const char* operator()(const polygon_vt&) {
            return "Polygon";
        }

        const char* operator()(const multi_point_vt&) {
            return "MultiPoint";
        }

        const char* operator()(const multi_line_string_vt&) {
            return "MultiLineString";
        }

        const char* operator()(const multi_polygon_vt&) {
            return "MultiPolygon";
        }

        const char* operator()(const geometry_collection_vt&) {
            return "GeometryCollection";
        }
    };

    struct to_coordinates_or_geometries {
        rapidjson_allocator& allocator;

        // Handles line_string, polygon, multi_point, multi_line_string, multi_polygon, and geometry_collection.
        template <class E>
        rapidjson_value operator()(const std::vector<E>& vector) {
            rapidjson_value result;
            result.SetArray();
            for (std::size_t i = 0; i < vector.size(); ++i) {
                result.PushBack(operator()(vector[i]), allocator);
            }
            return result;
        }

        rapidjson_value operator()(const point_vt& element) {
            rapidjson_value result;
            result.SetArray();
            result.PushBack(element.x, allocator);
            result.PushBack(element.y, allocator);
            return result;
        }

        rapidjson_value operator()(const empty_vt&) {
            abort();
        }

        rapidjson_value operator()(const geometry_vt& element) {
            return convert(element, allocator);
        }
    };

    struct to_value {
        rapidjson_allocator& allocator;

        rapidjson_value operator()(null_value_t) {
            rapidjson_value result;
            result.SetNull();
            return result;
        }

        rapidjson_value operator()(bool t) {
            rapidjson_value result;
            result.SetBool(t);
            return result;
        }

        rapidjson_value operator()(int64_t t) {
            rapidjson_value result;
            result.SetInt64(t);
            return result;
        }

        rapidjson_value operator()(uint64_t t) {
            rapidjson_value result;
            result.SetUint64(t);
            return result;
        }

        rapidjson_value operator()(double t) {
            rapidjson_value result;
            result.SetDouble(t);
            return result;
        }

        rapidjson_value operator()(const std::string& t) {
            rapidjson_value result;
            result.SetString(t.data(), rapidjson::SizeType(t.size()), allocator);
            return result;
        }

        rapidjson_value operator()(const std::vector<value>& array) {
            rapidjson_value result;
            result.SetArray();
            for (const auto& item : array) {
                rapidjson_value val;
                if (item.is<std::string>())
                {
                    auto& t = *item.getString();
                    val.SetString(t.data(), rapidjson::SizeType(t.size()), allocator);
                }
                else if (item.is<double>())
                {
                    auto t = *item.getDouble();
                    val.SetDouble(t);
                }
                else if (item.is<bool>())
                {
                    auto t = *item.getBool();
                    val.SetBool(t);
                }
                else if (item.is<int64_t>())
                {
                    auto t = *item.getInt();
                    val.SetInt64(t);
                }
                else if (item.is<uint64_t>())
                {
                    auto t = *item.getUint();
                    val.SetUint64(t);
                }
                else if (item.is<null_value_t>())
                {
                    val.SetNull();
                }
                result.PushBack(val, allocator);
                //result.PushBack(value::visit(item, *this), allocator);
            }
            return result;
        }

        rapidjson_value operator()(const std::unordered_map<std::string, value>& map) {
            rapidjson_value result;
            result.SetObject();
            for (const auto& [key, item] : map) {
                //result.AddMember(
                //    rapidjson::GenericStringRef<char> {
                //        property.first.data(),
                //        rapidjson::SizeType(property.first.size())
                //    },
                //    value::visit(property.second, *this),
                //    allocator);
                rapidjson_value val;
                if (item.is<std::string>())
                {
                    auto t = *item.getString();
                    val.SetString(t.data(), rapidjson::SizeType(t.size()), allocator);
                }
                else if (item.is<double>())
                {
                    auto t = *item.getDouble();
                    val.SetDouble(t);
                }
                else if (item.is<bool>())
                {
                    auto t = *item.getBool();
                    val.SetBool(t);
                }
                else if (item.is<int64_t>())
                {
                    auto t = *item.getInt();
                    val.SetInt64(t);
                }
                else if (item.is<uint64_t>())
                {
                    auto t = *item.getUint();
                    val.SetUint64(t);
                }
                else if (item.is<null_value_t>())
                {
                    val.SetNull();
                }
                result.AddMember(rapidjson::GenericStringRef<char> {
                    key.data(), rapidjson::SizeType(key.size())
                }, val, allocator);
            }
            return result;
        }
    };

    template <>
    rapidjson_value convert<geometry_vt>(const geometry_vt& element, rapidjson_allocator& allocator) {
        if (element.is<empty_vt>())
            return rapidjson_value(rapidjson::kNullType);

        rapidjson_value result(rapidjson::kObjectType);

        result.AddMember(
            "type",
            rapidjson::GenericStringRef<char> { geometry_vt::visit(element, to_type()) },
            allocator);

        result.AddMember(
            rapidjson::GenericStringRef<char> { element.is<geometry_collection_vt>() ? "geometries" : "coordinates" },
            geometry_vt::visit(element, to_coordinates_or_geometries{ allocator }),
            allocator);

        return result;
    }

    template <>
    rapidjson_value convert<feature_vt>(const feature_vt& element, rapidjson_allocator& allocator) {
        rapidjson_value result(rapidjson::kObjectType);
        result.AddMember("type", "Feature", allocator);

        if (!element.id.is<null_value_t>()) {
            result.AddMember("id", identifier::visit(element.id, to_value{ allocator }), allocator);
        }

        result.AddMember("geometry", convert(element.geometry, allocator), allocator);
        result.AddMember("properties", to_value{ allocator }(element.properties), allocator);

        return result;
    }

    template <>
    rapidjson_value convert<feature_collection_vt>(const feature_collection_vt& collection, rapidjson_allocator& allocator) {
        rapidjson_value result(rapidjson::kObjectType);
        result.AddMember("type", "FeatureCollection", allocator);

        rapidjson_value features(rapidjson::kArrayType);
        for (const auto& element : collection) {
            features.PushBack(convert(element, allocator), allocator);
        }
        result.AddMember("features", features, allocator);

        return result;
    }

    rapidjson_value convert(const geojson_vt& element, rapidjson_allocator& allocator) {
        return geojson_vt::visit(element, [&](const auto& alternative) {
            return convert(alternative, allocator);
            });
    }

    template <class T>
    std::string stringify(const T& t) {
        rapidjson_allocator allocator;
        rapidjson::StringBuffer buffer;
        rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
        convert(t, allocator).Accept(writer);
        return buffer.GetString();
    }

    std::string stringify(const geojson_vt& element) {
        return geojson_vt::visit(element, [](const auto& alternative) {
            return stringify(alternative);
            });
    }

} // namespace geojsonvt
} // namespace mapbox
#endif
